<?php

/*
 * File: config.php
 * Author: Andrew Saponenko <roguevoo@gmail.com>
 * Description: This file is a part of Zoombi PHP Framework
 */

if(!defined('ZBOOT'))
	return;

/**
 * Modules loader
 */
class ZLoader extends ZApplicationObject implements IZSingleton
{

	/**
	 * Array of plugins
	 * @var array
	 */
	private $m_plugins;

	/**
	 * Array of modules
	 * @var array
	 */
	private $m_modules;

	/**
	 * Array of models
	 * @var array
	 */
	private $m_models;

	/**
	 * Array of controllers
	 * @var array
	 */
	private $m_controllers;

	/**
	 * Array of loaded library holder
	 * @var array
	 */
	private $m_library;

	/**
	 * Singleton instance
	 * @var ZLoader
	 */
	static protected $m_instance = null;

	/**
	 * Constructor
	 */
	function __construct( ZObject & $a_parent = null, $a_name = null )
	{
		parent::__construct($a_parent, $a_name);
		$this->m_models = array();
		$this->m_controllers = array();
		$this->m_library = array();
		$this->m_plugins = array();
		$this->m_modules = array();
	}

	/**
	 * Protect from cloning
	 */
	private function __clone()
	{
		
	}

	/**
	 * Get ZLoader instance
	 * @return ZLoader
	 */
	static public function & getInstance()
	{
		if(self::$m_instance == null)
			self::$m_instance = new ZLoader;
		return self::$m_instance;
	}

	/**
	 * Find and instantize class
	 * @param string $a_name
	 * @param array $a_instance
	 * @param string $a_prefix
	 * @parem string $a_classname
	 * @return stdClass
	 */
	private function & _loadClass( $a_name, & $a_instances, $a_prefix, $a_baseclass )
	{
		$class_base = null;
		$class_name = (string)$a_name;

		$exp = explode(Zoombi::SS, $class_name);
		if(count($exp) > 1)
		{
			if($this->hasModule($exp[0]))
			{
				$module_name = array_shift($exp);
				$name = implode(Zoombi::DS, $exp);

				$module_instance = $this->module($module_name);
				if($module_instance)
				{
					$class_instance = $module_instance->getLoader()->_loadClass($name, $a_instances, $a_prefix, $a_baseclass); //$a_prefix($name);
					if($class_instance)
						return $class_instance;
				}
			}

			$class_name = array_pop($exp);
			$class_base = implode(Zoombi::DS, $exp);
		}

		$c_prefix = strtolower(trim((string)$a_prefix));
		$m_prefix = $c_prefix;

		if(empty($class_name))
			throw new ZLoaderException("{$m_prefix} class name must be not empty.");

		if($a_instances !== null AND isset($a_instances[$class_name]))
			return $a_instances[$class_name];

		$modprefix = null;
		if(
			$a_prefix == 'controller' AND
			$this->getModule()->getName() != $this->getModule()->getConfig()->getValue('module.default_name', 'index')
		)
			$modprefix = $this->getModule()->getName() . '_';

		$class_class = $this->getModule()->getConfig()->getValue($c_prefix . '.class_prefix') .
			$modprefix .
			$class_name .
			$this->getModule()->getConfig()->getValue($c_prefix . '.class_suffix', $m_prefix);


		if(class_exists($class_class))
		{
			if(!empty($a_baseclass) AND $class_class instanceof $a_baseclass)
				throw new ZLoaderException("{$m_prefix} class '{$class_class}' must be implement from '{$a_baseclass}'.");

			if($a_instances !== null)
			{
				$a_instances[$class_name] = new $class_class();
				return $a_instances[$class_name];
			}
		}

		$class_file = $this->getModule()->getConfig()->getValue($c_prefix . '.file_prefix') .
			$class_name .
			$this->getModule()->getConfig()->getValue($c_prefix . '.file_suffix') . '.' .
			$this->getModule()->getConfig()->getValue($c_prefix . '.file_extension', 'php');

		$class_dir = $this->getModule()->fromBaseDir($this->getModule()->getConfig()->getValue($c_prefix . '.directory_name', $c_prefix));

		if($a_prefix == 'module')
			$class_dir .= Zoombi::DS . $class_name;

		if(!empty($class_base))
			$class_dir .= Zoombi::DS . $class_base;

		$class_path = $class_dir . Zoombi::DS . $class_file;

		if(!file_exists($class_path))
			throw new ZLoaderException("File '{$class_path}' is not found for {$m_prefix} '{$class_class}' class.", ZLoaderException::EXC_NO_FILE);

		if(!is_readable($class_path))
			throw new ZLoaderException("{$m_prefix} class '{$class_class}' file not exist at '{$class_path}'.", ZLoaderException::EXC_NO_FILE);

		$inc_return = include_once($class_path);
		if(!$inc_return)
			throw new ZLoaderException("{$m_prefix} include path '{$class_path}' not included.");

		if(!class_exists($class_class))
			throw new ZLoaderException("{$m_prefix} class '{$class_class}' not found '{$class_path}'.");

		if(!is_subclass_of($class_class, $a_baseclass))
			throw new ZLoaderException("{$m_prefix} class '{$class_class}' must be implement from '{$a_baseclass}'.");

		$instance = new $class_class($this->getModule(), $class_name);
		if($a_instances !== null)
		{
			$a_instances[$class_name] = & $instance;
			return $a_instances[$class_name];
		}
		return $instance;
	}

	protected final function _loadFile( $a_name, $a_section, $a_title = 'ZLoader' )
	{
		$file_base = null;
		$file_name = (string)$a_name;
		$section = trim((string)$a_section);

		if(substr($file_name, 0, 1) == Zoombi::SS)
			return Zoombi::getApplication()->getLoader()->_loadFile(substr($file_name, 1), $a_section, $a_title);

		$exp = explode(Zoombi::SS, $file_name);
		if(count($exp) > 1)
		{
			$file_name = $exp[count($exp) - 1];
			$tm = $exp[0];
			if($this->hasModule($tm))
			{
				array_shift($exp);
				$f = $this->module($tm)->getLoader()->_loadFile(implode(Zoombi::SS, $exp), $a_section, $a_title);
				return $f;
			}
			else
			{
				array_pop($exp);
				$file_base = implode(Zoombi::DS, $exp);
			}
		}

		$file_dir = $this->getModule()->fromBaseDir($this->getModule()->getConfig()->getValue($a_section . 'directory_name', $section));
		if($file_base)
			$file_dir .= Zoombi::DS . $file_base;

		$file_path = $file_dir . Zoombi::DS . $file_name;
		if(file_exists($file_path) && is_file($file_path))
			return $file_path;

		$file_file = $this->getModule()->getConfig()->getValue($a_section . '.file_prefix') .
			$file_name .
			$this->getModule()->getConfig()->getValue($a_section . '.file_suffix');

		$file_path = $file_dir . Zoombi::DS . $file_file;

		if(file_exists($file_path) && is_file($file_path))
			return $file_path;

		$file_file .= '.' . $this->getModule()->getConfig()->getValue($a_section . '.file_extension', 'php');

		$file_path = $file_dir . Zoombi::DS . $file_file;

		if(!file_exists($file_path))
			throw new ZLoaderException("{$a_title}: '{$file_path}' is not exist.");

		if(!is_file($file_path))
			throw new ZLoaderException("{$a_title}: '{$file_path}' is not a file.");

		if(!is_readable($file_path))
			throw new ZLoaderException("{$a_title}: '{$file_path}' is not readable.");

		return $file_path;
	}

	/**
	 * Load controller
	 * @param string $a_controller Controller name
	 * @return ZController
	 */
	public final function & controller( $a_controller, $a_throw = false )
	{
		try
		{
			$this->emit(new ZEvent($this, 'preController', $a_controller));
			$ctl = $this->_loadClass($a_controller, $this->m_controllers, 'controller', 'ZController');
			$this->emit(new ZEvent($this, 'postController', $ctl->getName(), $a_controller));
			return $ctl;
		}
		catch(ZLoaderException $e)
		{
			if($a_throw)
				throw new ZControllerException($e->getMessage(), ZControllerException::EXC_LOAD);
		}
		return Zoombi::$null;
	}

	/**
	 * Load model
	 * @param string $a_model Model name
	 * @return ZModel
	 */
	public final function & model( $a_model, $a_throw = false )
	{
		try
		{
			return $this->_loadClass($a_model, $this->m_models, 'model', 'ZModel');
		}
		catch(ZLoaderException $e)
		{
			if($a_throw)
				throw new ZModelException($e->getMessage(), ZControllerException::EXC_LOAD);
		}
		return Zoombi::$null;
	}

	/**
	 * Load plugin
	 * @param string $a_plugin Plugin name
	 * @return ZPlugin
	 */
	public final function & plugin( $a_plugin, $a_throw = false )
	{
		try
		{
			$instance = $this->_loadClass($a_plugin, $this->m_plugins, 'plugin', 'ZPlugin');
			return $instance;
		}
		catch(ZLoaderException $e)
		{
			$filename = Zoombi::fromFrameworkDir('Plugins' . Zoombi::DS . $a_plugin . 'plugin.php');
			if(file_exists($filename))
			{
				include_once $filename;
				$classname = 'Z' . $a_plugin . 'Plugin';

				if(class_exists($classname))
				{
					$instance = new $classname($this->getModule(), $classname);
					return $instance;
				}
			}

			if($a_throw)
			{
				throw new ZPluginException($e->getMessage(), ZControllerException::EXC_LOAD);
			}
		}
		return Zoombi::$null;
	}

	/**
	 * Load action
	 * @param string $a_action A action name
	 * @return ZAction
	 */
	public final function action( $a_action, $a_throw = false )
	{
		try
		{
			return $this->_loadClass($a_action, $c, 'action', 'ZAction');
		}
		catch(ZLoaderException $e)
		{
			if($a_throw)
				throw new ZActionException($e->getMessage(), ZControllerException::EXC_LOAD);
		}
		return Zoombi::$null;
	}

	/**
	 * Find view file path
	 * @param string $a_view View name
	 * @return string
	 */
	public final function view( $a_view, $a_throw = false )
	{
		try
		{
			return $this->_loadFile($a_view, 'view', 'ZView');
		}
		catch(ZLoaderException $e)
		{
			if($a_throw)
				throw new ZViewException($e->getMessage(), ZControllerException::EXC_LOAD);
		}
		return Zoombi::$null;
	}

	/**
	 * Find and load module
	 * @param string $a_module
	 * @return ZModule
	 */
	public final function & module( $a_module, $a_throw = false )
	{
		$mod_name = trim(strtolower($a_module));
		if(isset($this->m_modules[$mod_name]) OR array_key_exists($mod_name, $this->m_modules))
			return $this->m_modules[$mod_name];

		$this->emit(new ZEvent($this, 'preModule', $mod_name));

		$mod_dir = $this->getModule()->fromModuleDir($mod_name);

		if(!file_exists($mod_dir))
			if($a_throw)
				throw new ZModuleException('Module "' . $mod_name . '" not exist', ZControllerException::EXC_LOAD);

		if(file_exists($mod_dir) AND is_dir($mod_dir))
		{
			$module = null;

			try
			{
				$module = $this->_loadClass($mod_name, $this->m_modules, 'module', 'ZModule');
			}
			catch(ZLoaderException $e)
			{
				if($a_throw)
					throw new ZModuleException($e->getMessage(), ZModuleException::EXC_LOAD);

				$module = new ZModule($this->getModule(), $mod_name);
				$this->m_modules[$mod_name] = $module;
			}
			$module->initialize();
			$module->setFlag('initialized');
			$this->emit(new ZEvent($this, 'postModule', $mod_name, $module));
			return $module;
		}
		return Zoombi::$null;
	}

	/**
	 * Check if module is exist
	 * @param string $a_module
	 * @return bool
	 */
	public function hasModule( $a_module )
	{
		$m = trim(strtolower($a_module));

		if(isset($this->m_modules[$m]))
			return true;

		$p = $this->getModule()->getConfig()->getValue('module.directory_name', ZModule::DEFAULT_MODULE_DIR) . Zoombi::DS . $m;
		$d = $this->getModule()->fromBaseDir($p);
		return file_exists($d);
	}

	/**
	 * Check if controller is exist
	 * @param string $a_controller
	 * @return bool
	 */
	public function hasController( $a_controller )
	{
		$c = trim(strtolower($a_controller));

		if(isset($this->m_controllers[$c]) OR array_key_exists($c, $this->m_controllers))
			return true;

		$file = null;
		try
		{
			$file = $this->_loadFile($c, 'controller');
		}
		catch(Exception $e)
		{
			
		}
		if(!$file)
			return false;

		return file_exists($file);
	}

	/**
	 * Find helper file path
	 * @param string $a_helper
	 * @return string
	 */
	public final function helper( $a_helper, $a_throw = false )
	{
		try
		{
			$filename = $this->_loadFile($a_helper, 'helper', 'Helper');
		}
		catch(ZLoaderException $e)
		{
			throw new ZHelperException($e->getMessage(), ZControllerException::EXC_LOAD);
		}

		if($filename)
		{
			include_once $filename;
			return true;
		}
	}

	/**
	 * Load from library
	 * @param string $a_path
	 * @return bool True if load success else return false
	 */
	static public final function library( $a_path )
	{
		$i = ZLoader::getInstance();

		if(key_exists($a_path, $i->m_library))
			return true;

		$i->m_library[$a_path] = true;

		$exp_path = explode('.', $a_path);
		$classname = array_pop($exp_path);

		$i->emit(new ZEvent($i, 'onLibrary', $classname));

		if($exp_path[0] == 'zoombi')
		{
			array_shift($exp_path);

			$new_path = array();
			foreach($exp_path as $p)
				array_push($new_path, ucwords($p));

			$exp_path = $new_path;
			$new_path = ( is_array($exp_path) ) ? implode(Zoombi::DS, $exp_path) : $exp_path;

			$filename = Zoombi::fromFrameworkDir($new_path . Zoombi::DS . strtolower($classname) . '.php');

			$code = null;

			if(file_exists($filename))
				$code = include_once( $filename );

			if($code != -1)
			{
				$i->emit(new ZEvent($i, 'onLibrarySucess', $classname));
				return true;
			}
		}
		else
		{
			$new_path = implode(Zoombi::DS, $exp_path);
			$filename = Zoombi::fromFrameworkDir($new_path . Zoombi::DS . $classname . '.php');

			$code = $this->loadFile($filename);
			if($code)
			{
				if(class_exists($classname))
					$i->emit(new ZEvent($i, 'onLibrarySucess', $classname));
				return true;
			}
		}
		$i->emit(new ZEvent($i, 'onLibraryFailed', $classname));
		return false;
	}

	/**
	 * Class autoloader
	 * @param string $a_class Class name
	 */
	public final function autoload( $a_class )
	{
		$this->emit(new ZEvent($this, 'onAutoload', $a_class));
		if($a_class[0] == 'Z')
		{
			$filename = strtolower(substr($a_class, 1, strlen($a_class) - 1));
			$base = Zoombi::getFrameworkDir();
			foreach(scandir($base) as $dir)
			{
				if($dir[0] == '.')
					continue;

				$path = $base . Zoombi::DS . $dir;
				if(is_dir($path) == false)
					continue;

				$filepath = $path . Zoombi::DS . $filename . '.php';
				if(is_file($filepath))
				{
					require_once $filepath;
					$this->emit(new ZEvent($this, 'onAutoloadSuccess', $a_class));
					return;
				}
			}
		}
		$this->emit(new ZEvent($this, 'onAutoloadFailed', $a_class));
	}

	public final function & factory( $a_name )
	{
		$n = 'Z' . $a_name;
		$c = Zoombi::null();
		try
		{
			$c = new $n;
		}
		catch(Exception $e)
		{
			Zoombi::getApplication()->triggerError($e);
		}
		return $c;
	}

}
